當 CLI 只輸出單色純文字時,多欄資料容易擠在一起難以閱讀,長時間執行的任務也會因為畫面靜止而無法判斷進度。為了解決這些閱讀與反饋問題,我們可以透過設計視覺回饋機制來改善終端機介面:用表格對齊結構化資料、用顏色標示執行狀態與關鍵字,並用 spinner 或進度條呈現任務執行進度。
本文示範的完整程式主要收錄在 cli-sample/output-format/:cmd/color.go 與 cmd/theme.go 負責狀態上色與共用主題樣式,cmd/table.go 示範 tablewriter 表格對齊,cmd/pterm.go 示範 spinner 等待動畫與進度條;區塊邊框排版範例則位於 cli-sample/interactive-ui/ 的 cmd/lipgloss_demo.go。
cli-sample/output-format/ 的目錄結構如下:
cli-sample/output-format/
├── cmd/
│ ├── root.go
│ ├── color.go
│ ├── theme.go
│ ├── table.go
│ └── pterm.go
├── go.mod
└── main.go
顏色可以用來標示訊息類型,以及檔案路徑、URL、資源 ID 等關鍵資訊。同一類資訊使用一致的顏色,使用者就能在多行輸出中快速找到相同類型的內容;例如替錯誤訊息套用同一種樣式,或只替說明文字中的資源 ID 上色。訊息仍要保留 Error:、Warning: 或 Success: 等文字前綴,讓關閉顏色後的輸出具備相同語意。
以下用資料寫入成功、找不到快取與伺服器連線失敗三種結果示範狀態上色。使用 fatih/color 的 color.Green、color.Yellow 與 color.Red 輸出訊息,終端機就會分別顯示綠色、黃色與紅色文字:
package cmd
import (
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var colorCmd = &cobra.Command{
Use: "color",
Short: "fatih/color 上色示範",
Run: func(cmd *cobra.Command, args []string) {
color.Green("Success: 資料已寫入")
color.Yellow("Warning: 找不到快取,改從伺服器讀取")
color.Red("Error: 無法連線到伺服器")
bold := color.New(color.Bold)
bold.Println("設定檔:~/.mycli/config.yaml")
},
}
func init() {
rootCmd.AddCommand(colorCmd)
}
這段程式位於 cli-sample/output-format/cmd/color.go。進入 cli-sample/output-format/ 後執行 go run . color,可以看到三種狀態顏色與粗體文字。

color.New 可以組合前景色、背景色、粗體與底線:
link := color.New(color.FgCyan, color.Underline)
link.Println("https://example.com/deploy/123")
當標準輸出透過 pipe(管道)交給另一個命令,或透過重導向寫入檔案時,輸出目的地不再是終端機。fatih/color 會自動關閉顏色,避免 ANSI 顏色碼混進下一個命令或檔案。直接在終端機執行時,也可以設定 NO_COLOR,讓命令只輸出純文字:
NO_COLOR=1 mycli deploy
如果每個子命令都直接呼叫 color.Red 或 color.Green,顏色設定會散落在各個檔案中,日後難以統一維護。將成功、警告與錯誤等樣式集中在 cmd/theme.go,並依訊息語意命名;呼叫端只需依語意取用,更換配色時也只要修改 cmd/theme.go:
package cmd
import "github.com/fatih/color"
var (
SuccessText = color.New(color.FgGreen, color.Bold)
ErrorText = color.New(color.FgRed)
WarningText = color.New(color.FgYellow)
)
呼叫端不需要知道 SuccessText 使用哪個 ANSI 顏色:
SuccessText.Println("資料庫連線成功")
ErrorText.Printf("找不到檔案:%s\n", filename)
固定前綴也可以收進函式,讓所有錯誤採用相同格式:
func PrintError(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
ErrorText.Printf("Error: %s\n", msg)
}
func PrintSuccess(msg string) {
SuccessText.Printf("Success: %s\n", msg)
}
當 CLI 需要把相關文字放在同一個區塊時,可以用邊框區隔前後輸出,再用內距避免文字貼住邊框。Lipgloss 可以替終端機文字設定邊框、內距、寬度、對齊與顏色,也能將多個區塊水平或垂直排列。
下面的 boxStyle 會替文字加上圓角邊框、左右留白與前景色:
import "github.com/charmbracelet/lipgloss"
boxStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()). // 使用圓角邊框
Padding(0, 1). // 上下 0 格,左右 1 格
Foreground(lipgloss.Color("12")) // 前景色使用 ANSI 色彩編號 12
// Render 套用 boxStyle 並回傳排版後的字串。
fmt.Println(boxStyle.Render("Deploying to production..."))
Render 不會直接輸出內容,而是回傳套用樣式後的字串,再由 fmt.Println 寫到終端機。輸出會顯示成一個獨立的圓角區塊:
╭──────────────────────────────╮
│ Deploying to production... │
╰──────────────────────────────╯
範例放在 interactive-ui/cmd/lipgloss_demo.go,共用樣式定義在同目錄的 style.go。進入 interactive-ui/ 後執行 go run . lipgloss,可以查看上色、圓角邊框與並排版面。

狀態樣式也可以使用同一個 Style API:
errorStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("9")).
Bold(true)
successStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("10"))
warningStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("11"))
fmt.Println(successStyle.Render("Success: deployment completed"))
fmt.Println(errorStyle.Render("Error: connection failed"))
fmt.Println(warningStyle.Render("Warning: using default config"))
邊框適合把同一組相關資訊和前後輸出分開。逐行日誌會持續增加,如果每行都加上邊框,不但佔用更多行數,也會讓內容更難掃讀;保留狀態前綴,並只替需要辨識的內容上色即可。
當 CLI 輸出含有多個欄位的結構化資料時,如果只用空白或定位點分隔,資料長度不一會讓欄位無法縱向對齊。使用 tablewriter 可以在輸出前自動計算每個欄位的最大寬度,補齊空格並繪製邊框,讓標頭與資料列保持縱向對齊:
package cmd
import (
"os"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var tableCmd = &cobra.Command{
Use: "table",
Short: "tablewriter 表格輸出示範(v1.x API)",
Run: func(cmd *cobra.Command, args []string) {
// 建立 Writer 並指定輸出目的地(標準輸出)
table := tablewriter.NewWriter(os.Stdout)
// 設定表格標頭
table.Header([]string{"Name", "Status", "Score"})
// 逐筆加入資料列
table.Append([]string{"Alice", "Active", "95"})
table.Append([]string{"Bob", "Inactive", "60"})
// 計算欄寬並繪製表格
table.Render()
},
}
輸出結果:
┌───────┬──────────┬───────┐
│ NAME │ STATUS │ SCORE │
├───────┼──────────┼───────┤
│ Alice │ Active │ 95 │
│ Bob │ Inactive │ 60 │
└───────┴──────────┴───────┘
範例放在 output-format/cmd/table.go。進入 output-format/ 後執行 go run . table,可以查看欄寬與框線的渲染結果。

命令執行時間較長時,可以用 spinner 顯示程式仍在執行,或用進度條顯示完成比例。工作總量未知時使用 spinner;總量已知時使用進度條。pterm 提供 spinner、進度條和表格等終端機元件:
package cmd
import (
"time"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
var ptermCmd = &cobra.Command{
Use: "pterm",
Short: "pterm 進度元件示範",
Run: func(cmd *cobra.Command, args []string) {
spinner, _ := pterm.DefaultSpinner.Start("正在建立部署")
time.Sleep(1 * time.Second)
spinner.Success("部署已建立")
bar, _ := pterm.DefaultProgressbar.WithTotal(100).Start()
for i := 0; i < 100; i++ {
bar.Increment()
}
pterm.DefaultTable.
WithHasHeader().
WithData(pterm.TableData{
{"Name", "Status", "Region"},
{"api", "Ready", "ap-east-1"},
{"worker", "Ready", "ap-east-1"},
}).
Render()
},
}
範例放在 output-format/cmd/pterm.go。進入 output-format/ 後執行 go run . pterm,spinner、進度條與表格會依序輸出。

完成顏色、表格與進度等輸出呈現後,下一篇我們接著進入輸入階段的互動設計,介紹如何在 Enter 送出前透過 Shell 自動完成補齊參數,以及在 Enter 送出後利用方向鍵選單與表單引導使用者操作。